Čeština

Komplexní průvodce generiky v TypeScriptu, který pokrývá jejich syntaxi, výhody, pokročilé použití a osvědčené postupy pro práci s komplexními datovými typy.

TypeScript Generics: Zvládnutí komplexních datových typů pro robustní aplikace

TypeScript, nadmnožina JavaScriptu, umožňuje vývojářům psát robustnější a udržovatelnější kód díky statickému typování. Mezi jeho nejmocnější funkce patří generika, která vám umožňují psát kód, jenž může pracovat s různými datovými typy a přitom stále zachovávat typovou bezpečnost. Tento průvodce poskytuje komplexní pohled na generika v TypeScriptu se zaměřením na jejich aplikaci na komplexní datové typy v kontextu globálního vývoje softwaru.

Co jsou generika?

Generika poskytují způsob, jak psát znovupoužitelný kód, který může pracovat s různými typy. Místo psaní samostatných funkcí nebo tříd pro každý typ, který chcete podporovat, můžete napsat jedinou funkci nebo třídu, která používá typové parametry. Tyto typové parametry jsou zástupnými symboly pro skutečné typy, které budou použity při volání nebo instanciování funkce či třídy. To je obzvláště užitečné při práci s komplexními datovými strukturami, kde se typ dat v těchto strukturách může lišit.

Výhody používání generik

Základní syntaxe generik

Základní syntaxe generik zahrnuje použití lomených závorek (< >) k deklaraci typových parametrů. Tyto typové parametry se obvykle nazývají T, K, V atd., ale můžete použít jakýkoli platný identifikátor. Zde je jednoduchý příklad generické funkce:


function identity<T>(arg: T): T {
  return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);

console.log(myString); // Výstup: hello
console.log(myNumber); // Výstup: 123
console.log(myBoolean); // Výstup: true

V tomto příkladu <T> deklaruje typový parametr s názvem T. Funkce identity přijímá argument typu T a vrací hodnotu typu T. Při volání funkce můžete explicitně specifikovat typový parametr (např. identity<string>) nebo nechat TypeScript, aby ho odvodil na základě typu argumentu.

Práce s komplexními datovými typy

Generika se stávají obzvláště cennými při práci s komplexními datovými typy, jako jsou pole, objekty a rozhraní. Pojďme se podívat na některé běžné scénáře:

Generická pole

Generika můžete použít k vytváření funkcí nebo tříd, které pracují s poli různých typů:


function arrayToString<T>(arr: T[]): string {
  return arr.join(", ");
}

let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];

console.log(arrayToString(numberArray)); // Výstup: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Výstup: apple, banana, cherry

Zde funkce arrayToString přijímá pole typu T[] a vrací řetězcovou reprezentaci pole. Tato funkce funguje s poli jakéhokoli typu, což ji činí vysoce znovupoužitelnou.

Generické objekty

Generika lze také použít k definování funkcí nebo tříd, které pracují s objekty různých tvarů:


interface Person {
  name: string;
  age: number;
  country: string; // Přidána země pro globální kontext
}

interface Product {
  id: number;
  name: string;
  price: number;
  currency: string; // Přidána měna pro globální kontext
}

function displayInfo<T extends { name: string }>(item: T): void {
  console.log(`Name: ${item.name}`);
}

let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };

displayInfo(person); // Výstup: Name: Alice
displayInfo(product); // Výstup: Name: Laptop

V tomto příkladu funkce displayInfo přijímá objekt typu T, který musí mít vlastnost name typu string. Klauzule extends { name: string } je omezení (constraint), které specifikuje minimální požadavky na typový parametr T. Tím je zajištěno, že funkce může bezpečně přistupovat k vlastnosti name.

Pokročilé použití generik

TypeScript generika nabízejí pokročilejší funkce, které vám umožňují vytvářet ještě flexibilnější a výkonnější kód. Pojďme se na některé z těchto funkcí podívat:

Více typových parametrů

Můžete definovat funkce nebo třídy s více typovými parametry:


function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

interface Name {
  firstName: string;
}

interface Age {
  age: number;
}

const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };

const merged = merge(person, details);
console.log(merged.firstName); // Výstup: Bob
console.log(merged.age); // Výstup: 42

Funkce merge přijímá dva objekty typů T a U a vrací nový objekt, který obsahuje vlastnosti obou objektů. Je to mocný způsob, jak kombinovat data z různých zdrojů.

Generická omezení

Jak bylo ukázáno dříve, omezení vám umožňují omezit typy, které lze použít s generickým typovým parametrem. Tím je zajištěno, že generický kód může bezpečně pracovat se specifikovanými typy.


interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]); // Výstup: 3
loggingIdentity("hello"); // Výstup: 5
// loggingIdentity(123); // Chyba: Argument typu 'number' není přiřaditelný parametru typu 'Lengthwise'.

Funkce loggingIdentity přijímá argument typu T, který musí mít vlastnost length typu number. Tím je zajištěno, že funkce může bezpečně přistupovat k vlastnosti length.

Generické třídy

Generika lze také použít s třídami:


class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data = this.data.filter(d => d !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Výstup: [ 'banana' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Výstup: [ 2 ]

Třída DataStorage může ukládat data jakéhokoli typu T. To vám umožňuje vytvářet znovupoužitelné datové struktury, které jsou typově bezpečné.

Generická rozhraní

Generická rozhraní jsou užitečná pro definování kontraktů, které mohou pracovat s různými typy. Například:


interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

interface User {
  id: number;
  username: string;
  email: string;
}

interface ErrorMessage {
  code: number;
  message: string;
}

function fetchUser(id: number): Result<User, ErrorMessage> {
  if (id === 1) {
    return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
  } else {
    return { success: false, error: { code: 404, message: "User not found" } };
  }
}

const userResult = fetchUser(1);
if (userResult.success) {
  console.log(userResult.data.username);
} else {
  console.log(userResult.error.message);
}

Rozhraní Result definuje generickou strukturu pro reprezentaci výsledku operace. Může obsahovat buď data typu T, nebo chybu typu E. Toto je běžný vzor pro zpracování asynchronních operací nebo operací, které mohou selhat.

Pomocné typy a generika

TypeScript poskytuje několik vestavěných pomocných typů, které dobře fungují s generiky. Tyto pomocné typy vám mohou pomoci transformovat a manipulovat s typy mocnými způsoby.

Partial<T>

Partial<T> učiní všechny vlastnosti typu T volitelnými:


interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // Platné

Readonly<T>

Readonly<T> učiní všechny vlastnosti typu T pouze pro čtení (readonly):


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Chyba: Nelze přiřadit k 'age', protože je to vlastnost pouze pro čtení.

Pick<T, K>

Pick<T, K> vybere sadu vlastností K z typu T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type NameAndAge = Pick<Person, "name" | "age">;

const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };

Omit<T, K>

Omit<T, K> odstraní sadu vlastností K z typu T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, "email">;

const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };

Record<K, T>

Record<K, T> vytvoří typ s klíči K a hodnotami typu T:


type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Rozšířený seznam pro globální kontext
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Rozšířený seznam pro globální kontext

type CurrencyMap = Record<CountryCodes, Currency>;

const currencyMap: CurrencyMap = {
  "US": "USD",
  "CA": "CAD",
  "UK": "GBP",
  "DE": "EUR",
  "FR": "EUR",
  "JP": "JPY",
  "CN": "CNY",
  "IN": "INR",
  "BR": "BRL",
  "AU": "AUD",
};

Mapované typy

Mapované typy vám umožňují transformovat existující typy iterací přes jejich vlastnosti. Je to mocný způsob, jak vytvářet nové typy na základě existujících. Můžete například vytvořit typ, který učiní všechny vlastnosti jiného typu pouze pro čtení:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Chyba: Nelze přiřadit k 'age', protože je to vlastnost pouze pro čtení.

V tomto příkladu [K in keyof Person] iteruje přes všechny klíče rozhraní Person a Person[K] přistupuje k typu každé vlastnosti. Klíčové slovo readonly činí každou vlastnost pouze pro čtení.

Podmíněné typy

Podmíněné typy vám umožňují definovat typy na základě podmínek. Je to mocný způsob, jak vytvářet typy, které se přizpůsobují různým scénářům.


type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string

function getValue<T>(value: T): NonNullable<T> {
  if (value == null) { // Zpracovává null i undefined
    throw new Error("Hodnota nemůže být null nebo undefined");
  }
  return value as NonNullable<T>;
}

try {
  const validValue = getValue("hello");
  console.log(validValue.toUpperCase()); // Výstup: HELLO

  const invalidValue = getValue(null); // Toto vyvolá chybu
  console.log(invalidValue); // Tento řádek nebude dosažen
} catch (error: any) {
  console.error(error.message); // Výstup: Hodnota nemůže být null nebo undefined
}

V tomto příkladu typ NonNullable<T> kontroluje, zda je T null nebo undefined. Pokud ano, vrací never, což znamená, že typ není povolen. V opačném případě vrací T. To vám umožňuje vytvářet typy, které jsou zaručeně ne-nullové.

Osvědčené postupy pro používání generik

Zde jsou některé osvědčené postupy, které je třeba mít na paměti při používání generik:

Příklady v globálním kontextu

Pojďme se podívat na několik příkladů, jak lze generika použít v globálním kontextu:

Převod měny


interface ConversionRate {
  rate: number;
  fromCurrency: string;
  toCurrency: string;
}

function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
  return amount * rate.rate;
}

const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD se rovná ${amountInEUR} EUR`); // Výstup: 100 USD se rovná 85 EUR

Formátování data


interface DateFormatOptions {
  locale: string;
  options: Intl.DateTimeFormatOptions;
}

function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
  return date.toLocaleDateString(format.locale, format.options);
}

const currentDate = new Date();

const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };

console.log("Datum v USA: " + formatDate(currentDate, usDateFormat));
console.log("Datum v Německu: " + formatDate(currentDate, germanDateFormat));
console.log("Datum v Japonsku: " + formatDate(currentDate, japaneseDateFormat));

Překladatelská služba


interface Translation {
  [key: string]: string; // Umožňuje dynamické jazykové klíče
}

interface LanguageData<T extends Translation> {
  languageCode: string;
  translations: T;
}

const englishTranslations: Translation = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our website!"
};

const spanishTranslations: Translation = {
  "hello": "Hola",
  "goodbye": "Adiós",
  "welcome": "¡Bienvenido a nuestro sitio web!"
};

const frenchTranslations: Translation = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre site web !"
};


const languageData: LanguageData<typeof englishTranslations>[] = [
  {languageCode: "en", translations: englishTranslations },
  {languageCode: "es", translations: spanishTranslations },
  {languageCode: "fr", translations: frenchTranslations}
];

function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
  const lang = languageData.find(lang => lang.languageCode === languageCode);
  if (!lang) {
    return `Překlad pro ${key} v ${languageCode} nebyl nalezen.`;
  }
  return lang.translations[key] || `Překlad pro ${key} nebyl nalezen.`;
}

console.log(translate("hello", "en", languageData)); // Výstup: Hello
console.log(translate("hello", "es", languageData)); // Výstup: Hola
console.log(translate("welcome", "fr", languageData)); // Výstup: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Výstup: Překlad pro missingKey v de nebyl nalezen.

Závěr

Generika v TypeScriptu jsou mocným nástrojem pro psaní znovupoužitelného, typově bezpečného kódu, který může pracovat s komplexními datovými typy. Porozuměním základní syntaxi, pokročilým funkcím a osvědčeným postupům pro používání generik můžete výrazně zlepšit kvalitu a udržovatelnost svých TypeScript aplikací. Při vývoji aplikací pro globální publikum vám generika mohou pomoci zpracovávat rozmanité datové formáty a kulturní konvence, čímž zajistíte bezproblémový uživatelský zážitek pro všechny.

TypeScript Generics: Zvládnutí komplexních datových typů pro robustní aplikace | MLOG